# Copyright (C) 2002-2011 The Written Word, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# $Id: __init__.py 798 2012-04-30 15:25:27Z china $

import atexit
import copy
import grp
import os
import pwd
import re
import shutil
import stat
import string
import sys

import depot
import misclib
import mysig
import prog.archive
import sbdb
import version

from misclib import VERBOSE_DEBUG, get_gnu_systype, systype_match
from textgroup import print_changelog, print_licenses, print_notes
from sbdb import SbDBInstallBase, SbDBInstallName
from version import Version, english_test_string

# constants
_state_index = { 'unpack' : 0, 'configure' : 1, 'build' : 2,
                 'test' : 3, 'install' : 4 }

# exceptions
class ProgError (Exception):
  """base class for all exceptions raised by this module"""

  def __init__ (self, arg = None):
    self.msg = arg

  def __str__ (self):
    return self.msg

class ProgArchiveError (ProgError):
  """error processing archive or with files in archive"""

class ProgArchiveInvalidError (ProgError):
  """invalid archive"""

class ProgArchiveMemberDoesNotExist (ProgError):
  """member does not exist in archive"""

class ProgBuildError (ProgError):
  """error building program"""

class ProgConfigureError (ProgError):
  """error configuring program"""

class ProgExtractError (ProgError):
  """error extracting member from archive"""

class ProgInstallError (ProgError):
  """error installing program"""

class ProgProgramNotFound (ProgError):
  """program not found"""

  def __init__ (self, prog):
    self.msg = 'unable to locate path to ' + prog + '. please set \
the environment variable in the config file correctly to locate the \
program.'

class ProgPurgeError (ProgError):
  """error purging program source"""

class ProgTestError (ProgError):
  """error testing program"""

class ProgUninstallError (ProgError):
  """error uninstalling program"""

class ProgUnpackError (ProgError):
  """error unpacking sources"""

class ProgUnpackFileError (ProgError):
  """error unpacking source file"""

class ProgValidateError (ProgError):
  """error validate sources"""

class ProgPayloadNotFound (ProgError):
  """source/patch payload not found"""

class ProgPayloadInvalidSize (ProgError):
  """invalid init/wrapper payload file size"""

  def __init__ (self, size):
    self.size = size

class ProgPayloadInvalidChecksum (ProgError):
  """invalid init/wrapper payload checksum"""

  def __init__ (self, checksum):
    self.checksum = checksum


# classes to store program and module name dependencies when
# dependent entries found during uninstall
class PrintProgDeps ():
  def __init__ (self):
    self.entries = {}

  def __getitem__ (self, key):
    return self.entries[key]

  def __nonzero__ (self):
    if self.entries:
      return True
    else:
      return False

  def __setitem__ (self, key, value):
    self.entries[key] = value

  def __str__ (self):
    str = []
    progs = self.entries.keys ()
    progs.sort ()
    for prog in progs:
      if self.entries[prog]:
        self.entries[prog].sort ()

        mods = []
        for mod in self.entries[prog]:
          mods.append ('"' + mod + '"')

        mod_str = prog + ' (module '
        mod_str += string.join (mods, ', ')
        mod_str += ')'

        str.append (mod_str)
      else:
        str.append (prog)

    return string.join (str, ', ')

  def has_key (self, key):
    return self.entries.has_key (key)

  def keys (self):
    return self.entries.keys ()

class PrintProgDepsToIgnore (PrintProgDeps):
  def __init__ (self):
    PrintProgDeps.__init__ (self)

class PrintProgDepsToUninstall (PrintProgDeps):
  def __init__ (self):
    PrintProgDeps.__init__ (self)


class Prog:
  def __init__ (self, program_name):
    self.name = program_name		# program name and version

    self.prog_name = None		# program name
    self.prog_ver = None		# program version
    self.prog_rev = None		# program revision

    self.archive = None			# archive file
    self.depot = None			# depot containing pb-db.xml entry
    self.sbdb = None			# pg-db.xml entry
    self.srcdir = None			# path to sources (could be path
					# to .sb archive)

    self.builddir = None		# build directory
    self.dist = None			# distribution name
    self.dry_run = 0			# --dry-run command-line option
    self.ignoredeps = 0			# --ignoredeps command-line option
    self.environ = None			# environment vars for scripts
    self.install_base = None		# installation base
    self.install_path = None		# installation path
    self.install_prefix_base = None	# installation base for
					# installation prefix (when
					# <install-base
					#   install-prefix-only="1">
    self.lib_path_component = 'lib'	# path component to libraries
    self.lib_search_path_components = [ 'lib' ] # paths to search for
					# libraries
    self.tmpdir = None			# temporary working directory
    self.verbose = 0			# verbosity level

    self.bash_path = None		# path to 'bash' program

    self.modules = []

    self.quilt = 0			# fetch patches from quilt?
    self.quilt_patches = None		# directory for quilt patches

    self.rename_matching_install_prefix_entries = 0
    self.unique_install_prefix = 0

    self.sbdb_build_entries = None	# database entries in build directory
    self.sbdb_install_entries = None	# database entries in install directory

    self.umask = 022			# default umask

  def __deepcopy__ (self, memo):
    _prog = self.__class__ (self.name)
    memo[id (self)] = _prog

    for n, v in self.__dict__.iteritems ():
      if n in ['depot', 'environ', 'modules', 'sbdb']:
        setattr (_prog, n, v)
      else:
        setattr (_prog, n, copy.deepcopy (v, memo))

    return _prog

  # output depot path
  def __str__ (self):
    return self.name

  def build (self, callback = None):
    try:
      dependencies = prep (self, callback, run_state = 'build',
                           script = 'build')

      # if we reference a source file, extract source files if
      # input file is an archive
      for source in self.module.sources:
        if source[1].var:
          extract_source (self, callback)
          break

      script, script_name = create_script (self, callback, 'build',
                                           dependencies, 'gmake')

      execute_script (self, callback, 'build', script_name, dependencies)
    except ProgError:
      raise ProgBuildError

    # update database entry
    sbdb.add_sbdb_entry (self.sbdb_build_entries, 'build', callback, self)

  def exists_in_build_db (self):
    try:
      sbdb_build_entry = self.sbdb_build_entries[self.prog_name +
                                                 '-' + self.prog_ver]
    except IndexError:
      return False

    return True

  # fetch program from remote repository and validate
  def fetch_and_verify (self, fetch_callback, show_progress = True):
    # if not reporting progress, report errors but remove indent
    indent = 0
    if not show_progress:
      indent = -2

    retrieve_program = False
    if self.depot.exists (self.name + '.sb'):
      if show_progress:
        fetch_callback ('begin', 'checking if program exists in local depot')
      if self.depot.prog_exists (self.name):
        if show_progress:
          fetch_callback ('end', 'yes')

        try:
          self.archive = prog.archive.init (self.depot.local_path +
                                            '/' + self.name + '.sb')
        except prog.ProgArchiveInvalidError, msg:
          if show_progress:
            fetch_callback ('begin', 'validating archive')
            fetch_callback ('end', 'failed')
            fetch_callback ('error', str (msg))
          retrieve_program = True

        if self.archive:
          self.archive.path = self.depot.local_path + '/' + self.name + '.sb'

          # check version in local depot against version in remote
          # depot. if version in remote depot newer, retrieve,
          # else use version in local depot. any error causes us
          # to use version in local depot.
          if show_progress:
            fetch_callback ('begin', 'comparing checksum of \
program in local depot against depot')
          try:
            if self.depot.local_uptodate (self.name):
              if show_progress:
                fetch_callback ('end', 'ok')
            else:
              if show_progress:
                fetch_callback ('end', 'older')
              retrieve_program = True
          except depot.DepotFileNotFoundError:
            if show_progress:
              fetch_callback ('end', 'failed')
            fetch_callback ('error', 'error communicating \
with remote depot\n', indent)
            raise
      else:
        if show_progress:
          fetch_callback ('end', 'no')
        retrieve_program = True

    if retrieve_program:
      # if program "supposedly" exists (because entry exists in
      # pb-db.xml file) but not present in depot, error. possible
      # because pb-db.xml file is downloaded with programs downloaded
      # from a remote depot.
      if not self.depot.is_remote ():
        fetch_callback ('error', 'not found in depot ' +
                        str (self.depot) + '. this should not happen. \
one possibility is that ' + str (self.depot) + ' was created by \
initially specifying a remote depot and having pb populate programs \
in the local depot from the remote depot. this program was then not \
specified in the list of programs to install. when specifying a \
remote depot, the pb-db.xml file listing all programs in the remote \
depot is downloaded to the local depot. unless all programs are also \
downloaded, the pb-db.xml file will be out of sync with the \
programs that are available in the local depot.', indent)
        raise depot.DepotFileNotFoundError

      if show_progress:
        fetch_callback ('end', 'retrieving ' + self.prog_name + '-' + \
                                self.prog_ver + '-' + str (self.prog_rev) + \
                                ' from remote depot ...')
      try:
        self.depot.get (self.name, show_progress)
      except depot.DepotFileNotFoundError, msg:
        fetch_callback ('error', msg.file + ' not found in depot', indent)
        raise

      self.archive = prog.archive.init (self.depot.local_path + '/' +
                                        self.name + '.sb')
      self.archive.path = self.depot.local_path + '/' + self.name + '.sb'

  def test (self, callback = None):
    try:
      dependencies = prep (self, callback, run_state = 'build',
                           script = 'test')

      # if we reference a source file, extract source files if
      # input file is an archive
      for source in self.module.sources:
        if source[1].var:
          extract_source (self, callback)
          break

      script, script_name = create_script (self, callback, 'test',
                                           dependencies)

      execute_script (self, callback, 'test', script_name, dependencies)
    except ProgError:
      raise ProgTestError

    # update database entry
    sbdb.add_sbdb_entry (self.sbdb_build_entries, 'build', callback, self)

  # remove payload source files
  def cleanup (self, callback):
    # we no longer need the signal handler registered earlier by the
    # extract_source() function at the end of this file...
    mysig.unregister (self.cleanup)

    # ...regardless of the contents of `self.archive`.
    if not self.archive:
      return

    callback ('begin', 'removing extracted source from ' + self.srcdir)
    misclib.rmdir (self.srcdir)
    callback ('end', 'done')

  def configure (self, callback = None):
    if not self.module.proc['configure']:
      return

    try:
      dependencies = prep (self, callback, run_state = 'build',
                           script = 'configure')

      # if we reference a source file, extract source files if
      # input file is an archive
      for source in self.module.sources:
        if source[1].var:
          extract_source (self, callback)
          break

      script, script_name = create_script (self, callback, 'configure',
                                           dependencies)

      execute_script (self, callback, 'configure', script_name, dependencies)
    except ProgError:
      raise ProgConfigureError

    # update database entry
    sbdb.add_sbdb_entry (self.sbdb_build_entries, 'build', callback, self)

  # installation path
  def get_install_path (self, install_name = None):
    if not install_name:
      install_name = self.module.install_name

    if not install_name.version:
      version = self.prog_ver
    else:
      version = install_name.version

    string = ''
    if install_name.path:
      string = re.sub ('\${SB_INSTALL_NAME}', install_name.path,
                       self.install_path)
      string = re.sub ('\${INSTALL_NAME}', install_name.path, string)
      string = re.sub ('\${SB_PROG_VER}', version, string)
      string = re.sub ('\${VER}', version, string)

    # append string to install path
    if install_name.append:
      return os.path.join (string, install_name.append)
    else:
      return string

  # installation prefix
  def get_install_prefix (self, install_base = None, install_name = None):
    if not install_base:
      install_base = self.install_prefix_base

    _install_prefix = os.path.join (install_base.path,
                                    self.get_install_path (install_name))
    return var_substitute (self, os.path.normpath (_install_prefix))

  def install (self, callback = None):
    try:
      dependencies = prep (self, callback, run_state = 'install',
                           script = 'install')

      # if we reference a source file, extract source files if
      # input file is an archive
      for source in self.module.sources:
        if source[1].var:
          extract_source (self, callback)
          break

      script, script_name = create_script (self, callback, 'install',
                                           dependencies, 'gmake install')

      execute_script (self, callback, 'install', script_name, dependencies)
    except ProgError:
      raise ProgInstallError

    # install license files
    if os.path.isdir (self.srcdir + '/license'):
      licenses = os.listdir (self.srcdir + '/license')
      if licenses:
        callback ('begin', 'copying license files')

        if not self.dry_run:
          install_prefix = self.get_install_prefix ()
          if os.path.isdir (install_prefix):
            if not os.path.exists (install_prefix + '/license'):
              os.mkdir (install_prefix + '/license', 0755)
            for license in licenses:
              shutil.copyfile (self.srcdir + '/license/' + license,
                               install_prefix + '/license/' + license)
              os.chmod (install_prefix + '/license/' + license, 0644)

        callback ('end', 'done')

    # update database entry
    sbdb.add_sbdb_entry \
      (self.sbdb_install_entries[self.install_prefix_base.path],
       'install', callback, self)

  def purge (self, callback = None):
    # we might be called by unpack() to purge a build entry with
    # a different module name. if the module in the build database
    # exists in the .sb file, automatically remove the old entry.
    try:
      sbdb_entry = self.sbdb_build_entries[self.prog_name + '-' +
                                           self.prog_ver]
    except IndexError:
      # only one other program in the build directory can have
      # a matching build prefix (the second causes the error
      # below)
      program_with_matching_build_prefix = ''

      for sbdb_entry in self.sbdb_build_entries:
        program = Prog (sbdb_entry.prog_name)
        program.builddir = self.builddir

        for module in sbdb_entry.modules.keys ():
          program.module = sbdb_entry.modules[module]
          program.prog_name = sbdb_entry.prog_name
          program.prog_ver = sbdb_entry.prog_ver
          program.prog_rev = sbdb_entry.prog_rev
          program.build_prefix = program.get_build_prefix ()

          if program.build_prefix == self.build_prefix and \
          self.builddir != self.build_prefix:
            program_with_matching_build_prefix = program.prog_name + \
                                                 '-' + program.prog_ver
            break
        else:
          continue

        break

      if program_with_matching_build_prefix:
        callback ('error', 'unable to remove ' + self.build_prefix + 
                  ' because ' + program_with_matching_build_prefix +
                          ' is unpacked to the same build directory. \
please purge ' + program_with_matching_build_prefix +
                  ' before trying to unpack this program.', -2)
      else:
        callback ('error', 'unable to remove ' +
                          self.build_prefix + ' because a program has \
already been unpacked to this directory. either the program was \
manually unpacked or the unpack procedure failed. please remove the \
directory and try again.', -2)

      raise ProgPurgeError

    if not sbdb_entry.modules.has_key (self.module.name):
      for module in sbdb_entry.modules.keys ():
        if sbdb_entry.modules[module].build_name == self.module.build_name:
          break
      else:
        callback ('error', 'unable to remove ' + self.build_prefix +
                  ' because a program has already been unpacked to \
this directory. either the program was manually unpacked or the unpack \
procedure failed. please remove the directory and try again.', -2)
        raise ProgPurgeError

      if self.sbdb.modules.has_key (module):
        callback ('warning', 'the "' + self.module.name + '" module has \
not been unpacked to ' + self.build_prefix + '. the "' + module + '" \
module is currently unpacked to this directory. the "' + module + '" \
module will automatically be removed using the module-specific <purge> \
entry or the <purge> entry for the "default" module from the same .sb \
file where this "' + self.module.name + '" module was found.', -2)
      else:
        callback ('error', 'the "' + self.module.name + '" module has \
not been unpacked to ' + self.build_prefix + '. the "' + module + '" \
module is currently unpacked to this directory. normally, the "' +
                  module + '" module would automically be purged but \
an entry for the "' + module + '" module no longer exists in the same \
.sb file where this "' + self.module.name + '" module was found. \
therefore, the build directory ' + self.build_prefix + ' must be manually \
removed and the module entry removed from the database file ' +
                          os.path.join (str (self.sbdb_build_entries),
                                self.prog_name + '-' + self.prog_ver) + '.',
                  -2)
        raise ProgPurgeError

      program = copy.deepcopy (self)
      program.module = program.sbdb.modules[module]
      program.prog_systype = program.module.systype
      program.sbdb_build_entries = self.sbdb_build_entries
      program.purge (callback)

      return

    # unless --ignoredeps given and program has dependents,
    # don't remove
    callback ('begin', 'checking for programs dependent on me')

    sbdb_purge_entry = self.sbdb_build_entries[self.prog_name + '-' +
                       self.prog_ver]
    sbdb_purge_module = sbdb_purge_entry.modules[self.module.name]
    versions = []

    for sbdb_entry in self.sbdb_build_entries:
      for module in sbdb_entry.modules.keys ():
        sbdb_module = sbdb_entry.modules[module]

        for depend in get_dependency_list (None,
                                           sbdb_module.dependencies,
                                           sbdb_entry.d_dependencies):
          # skip user/group dependencies
          if depend.user or depend.group:
            continue

          # skip installation dependencies
          if depend.state == 'install':
            continue

          # match build dependencies with correct program name
          if depend.program == sbdb_purge_entry.prog_name and \
          depend.module == self.module.name:
            if version.met (depend.test.split (),
                            Version (self.sbdb.prog_ver),
                            Version (self.sbdb.prog_rev)) and \
            _state_index[sbdb_purge_module.state] >= _state_index[depend.state]:
              if self.module.name != "default":
                versions.append (sbdb_entry.prog_name + '-' + \
                                 sbdb_entry.prog_ver + ' (module="' +
                                 self.module.name + '")')
              else:
                versions.append (sbdb_entry.prog_name + '-' + \
                                 sbdb_entry.prog_ver)
              break
        else:
          continue

        break

    versions.sort ()
    if versions:
      callback ('end', 'yes')
    else:
      callback ('end', 'none')

    if versions and not self.ignoredeps:
      callback ('error', 'unable to purge because the \
following programs are dependent on me (use --ignoredeps to force \
purge): ' + string.join (versions, ', ') + '.')

      raise ProgPurgeError

    if self.module.proc['purge']:
      script, script_name = create_script (self, callback, 'purge', None)

      try:
        execute_script (self, callback, 'purge', script_name, None)
      except ProgError:
        raise ProgPurgeError
    else:
      if os.path.exists (self.build_prefix) and \
      self.builddir != self.build_prefix:
        callback ('begin', 'removing ' + self.build_prefix)
        if not self.dry_run:
          os.chdir (self.builddir)
          misclib.rmdir (self.build_prefix)
        callback ('end', 'done')

    sbdb.remove_sbdb_build_entry (self.sbdb_build_entries, callback, self)

  def query (self, query_items, query_indent, module):
    if not self.module.install_name:
      return

    for query_item in query_items:
      if query_item == 'changelog':
        print_changelog (self, query_indent)
      elif query_item == 'install-prefix':
        print self.get_install_prefix ()
      elif query_item == 'install-name':
        print var_substitute (self, self.module.install_name.path)
      elif query_item == 'licenses':
        print_licenses (self, query_indent)
      elif query_item == 'module-names':
        print module
      elif query_item == 'notes':
        print_notes (self, query_indent)
      elif query_item == 'patches':
        for source in self.module.sources:
          type = source[0]			# 'source' or 'patch'
          path = var_substitute (self, source[1].path)

          if type == 'patch':
            # path either absolute or relative to depot directory
            # where xml file found
            if os.path.isabs (path):
              src = path
            else:
              src = self.srcdir + '/' + path

            # quilt-style patches?
            if src.endswith ('/'):
              if self.quilt and os.path.isdir (src):
                series = os.path.join (src, self.quilt_patches, "series")
                fh = open (series, 'r')

                while 1:
                  patch_file = fh.readline ().strip ().split (' ')[0]
                  if not patch_file:
                    break

                  print os.path.join (src, self.quilt_patches, patch_file)

                fh.close ()
              elif not self.quilt:
                raise ProgUnpackError ('patch file "' + src +
                                       '" ends with a "/" but the \
"quilt" variable in the config file is not set.')
              elif not os.path.isdir (src):
                raise ProgUnpackError ('patch file "' + src +
                                       '" ends with a "/" but it is \
not a directory.')
            else:
              print src
      elif query_item == 'program-name':
        print self.prog_name
      elif query_item == 'program-version':
        print self.prog_ver
      elif query_item == 'sbdb-path':
        print os.path.normpath (os.path.join (self.depot.local_path,
                                              self.sbdb.filename))
      elif query_item == 'sources':
        for source in self.module.sources:
          type = source[0]			# 'source' or 'patch'
          path = var_substitute (self, source[1].path)

          if type == 'source':
            if os.path.isabs (path):
              print path
            else:
              print self.srcdir + '/' + path

  def validate (self, module, callback):
    if self.module.validate:
      dependencies = self.module.validate
    else:
      dependencies = self.sbdb.d_validate

    if not dependencies:
      callback ('warning', 'no <validate> section for the "' +
                        module + '" module nor a default <validate> \
section. therefore, i will check if an entry exists in the \
installation database for this module.', -2)

      # a noop as we could not reach this code path if an entry
      # did not exist. so, fake the search.
      callback ('begin', 'searching for "' + self.prog_name +
                '" (module="' + self.module.name + '")')
      callback ('end', 'found')

      return

    # verify dependencies in <validate> section
    if self.module.build and self.module.build_name and \
    self.module.install_name:
      callback ('begin', 'searching for "' + self.prog_name +
                '" (module="' + self.module.name + '")')
      callback ('end', 'found')

    try:
      verify_dependencies (self, dependencies, 'install', callback,
                           indent = -2, print_path_if_found = 0)
    except ProgError:
      raise ProgValidateError

  # get build prefix
  def get_build_prefix (self, build_name = None):
    if build_name is None:
      build_name = self.module.build_name

    return os.path.normpath (os.path.join (self.builddir,
                                           var_substitute (self, build_name)))

  def uninstall (self, callback = None):
    module_names = []

    # only uninstall programs that have been installed
    for sbdb_entry in self.sbdb_install_entries[self.install_base.path]:
      if sbdb_entry.prog_name == self.prog_name and \
      sbdb_entry.prog_ver == self.prog_ver:
        if sbdb_entry.modules.has_key (self.module.name):
          module_names.append (self.module.name)
          break

        # allow uninstalling if module is 'default' even if no
        # 'default' module exists. if .sb file contains only
        # modules, this is the only way to remove the program.
        if self.module.name == 'default' and \
        self.module.proc['uninstall']:
          for module_name in sbdb_entry.modules.keys ():
            module_names.append (module_name)
          break
    else:
      versions = []
      for sbdb_entry in self.sbdb_install_entries[self.install_base.path]:
        if sbdb_entry.prog_name == self.prog_name:
          versions.append (sbdb_entry.prog_name + '-' +
                           sbdb_entry.prog_ver + ' (modules="' +
                           string.join (sbdb_entry.modules.keys (),
                                        ', ') + '")')

      if versions:
        callback ('error', 'unable to uninstall as no entry \
exists in the database. the versions I know about are: ' +
                  string.join (versions, ', ') + '.', -2)
      else:
        callback ('error', 'no module installed to ' +
                  self.get_install_prefix (), -2)
      raise ProgUninstallError

    if self.module.name != 'default' and \
    not self.module.proc['uninstall']:
      # allow module removal if installation prefix different from
      # installation prefix of default module
      sbdb_module = self.sbdb.modules['default']
      if sbdb_module.build_name and sbdb_module.install_name:
        default_program = Prog (self.sbdb.prog_name + '-' +
                               self.sbdb.prog_ver)
        default_program.module = sbdb_module
        default_program.prog_name = self.sbdb.prog_name
        default_program.prog_ver = self.sbdb.prog_ver
        default_program.install_base = self.install_base
        default_program.install_path = self.install_path
        default_program.install_prefix_base = self.install_prefix_base

        if default_program.get_install_prefix () == \
        self.get_install_prefix ():
          callback ('error', 'the "' + self.module.name +
                            '" module does not contain an <uninstall> \
section. only non-default modules with <uninstall> sections can be \
removed.', -2)
          raise ProgUninstallError
      else:
        callback ('error', 'the "' + self.module.name +
                          '" module does not contain an <uninstall> \
section. only non-default modules with <uninstall> sections can be \
removed.', -2)
        raise ProgUninstallError

    # unless --ignoredeps given and program has dependents,
    # don't remove
    callback ('begin', 'checking for programs dependent on me')

    sbdb_uninstall_entry = \
      self.sbdb_install_entries[self.install_base.path][self.prog_name +
                                                        '-' + self.prog_ver]

    prog_deps = PrintProgDeps ()
    prog_deps_ignore = PrintProgDepsToIgnore ()
    prog_deps_uninstall = PrintProgDepsToUninstall ()

    for sbdb_entry in self.sbdb_install_entries[self.install_base.path]:
      # skip entry if it does not contain a module entry for the
      # module we are deleting; implies we are deleting the
      # 'default' module.
      if sbdb_entry.prog_name == self.prog_name and \
      sbdb_entry.prog_ver == self.prog_ver and \
      not sbdb_entry.modules.has_key (self.module.name):
        continue

      sbdb_entry_modules = sbdb_entry.modules.keys ()
      sbdb_entry_modules.sort ()
      for module in sbdb_entry_modules:
        sbdb_module = sbdb_entry.modules[module]

        for depend in get_dependency_list (None,
                                           sbdb_module.dependencies,
                                           sbdb_entry.d_dependencies):
          # skip user/group dependencies
          if depend.user or depend.group:
            continue

          # skip non-installation dependencies
          if depend.state != 'install' or depend.type != 'run':
            continue

          # skip dependencies with alternate installation base
          if depend.install_base and \
          depend.install_base != self.install_base:
            continue

          # platform-specific dependency
          if depend.systype:
            gnu_systype = get_gnu_systype (self.systype_path)
            if not systype_match (depend.systype, gnu_systype):
              continue

          # match installation dependencies with correct program name
          if sbdb_uninstall_entry.prog_name == depend.program:
            for module_name in module_names:
              if depend.module == module_name:
                if version.met (depend.test.split (),
                                Version (self.sbdb.prog_ver),
                                Version (self.sbdb.prog_rev)):

                  depend_program = Prog (sbdb_entry.prog_name + '-' +
                                         sbdb_entry.prog_ver)
                  depend_program.module = sbdb_module
                  depend_program.prog_name = sbdb_entry.prog_name
                  depend_program.prog_ver = sbdb_entry.prog_ver
                  depend_program.install_path = self.install_path

                  if depend.install_name.path:
                    depend_install_path = depend_program.get_install_path (depend.install_name)
                    if depend_install_path != var_substitute (self, self.get_install_path ()):
                      continue

                  prog_name = sbdb_entry.prog_name + '-' + \
                              sbdb_entry.prog_ver

                  # special case when we remove a program and
                  # it contains modules dependent on the default
                  # module. we would normall error out but we
                  # ignore these modules because removing the
                  # program would remove them as well.
                  if sbdb_entry.prog_name == self.prog_name and \
                  sbdb_entry.prog_ver == self.prog_ver and \
                  self.module.name == 'default' and \
                  sbdb_module.name != 'default':
                    prog_module = self.sbdb.modules[sbdb_module.name]
                    if prog_module.proc['uninstall']:
                      if not prog_deps_uninstall.has_key (prog_name):
                        prog_deps_uninstall[prog_name] = []

                      prog_deps_uninstall[prog_name].append (sbdb_module.name)
                    else:
                      if not prog_deps_ignore.has_key (prog_name):
                        prog_deps_ignore[prog_name] = []

                      prog_deps_ignore[prog_name].append (sbdb_module.name)
                  else:
                    if not prog_deps.has_key (prog_name):
                      prog_deps[prog_name] = []

                    if module_name != 'default':
                      prog_deps[prog_name].append (sbdb_module.name)
                    elif module != 'default':
                      prog_deps[prog_name].append (module)
            else:
              continue

            break
        else:
          continue

        break

    if prog_deps or prog_deps_ignore or prog_deps_uninstall:
      callback ('end', 'yes')
    else:
      callback ('end', 'none')

    if prog_deps_ignore:
      callback ('note', 'ignoring the following dependencies because \
removing this program will also remove them: ' +
                str (prog_deps_ignore) + '.')
    if prog_deps_uninstall:
      if prog_deps_ignore:
        callback ('error', '')

      callback ('error', 'unable to uninstall because the following \
modules for this program have an <uninstall> section. these modules \
must be uninstalled before the program can be uninstalled: ' +
                str (prog_deps_uninstall) + '.')
    if prog_deps and not self.ignoredeps:
      if prog_deps_ignore or prog_deps_uninstall:
        callback ('error', '')

      callback ('error', 'unable to uninstall because the \
following programs are dependent on me (use --ignoredeps to force \
uninstall): ' + str (prog_deps) + '.')

    if prog_deps_uninstall or (prog_deps and not self.ignoredeps):
      raise ProgUninstallError

    # installation database entries
    sbdb_entries = self.sbdb_install_entries[self.install_base.path]

    # collect entries to remove from install database
    remove_db_programs = []
    if self.module.proc['uninstall']:
      for remove_db_entry in self.module.proc['uninstall'].remove_db_entries:
        # validate test argument
        if remove_db_entry.test:
          for test in remove_db_entry.test.split ():
            test_var = re.sub ('^(\w)([>=<]+)(.*)', '\\1', test)
            test_compare = re.sub ('^(\w)([>=<]+)(.*)', '\\2', test)

            if (test_var != 'v' and test_var != 'r') or \
            (test_compare != '<' and test_compare != '>' and \
            test_compare != '<=' and test_compare != '>=' and \
            test_compare != '=='):
              callback ('error', 'remove-db-entry test not \
correctly formatted. while "' + test + '" was given, the correct \
format is: [v|r]"<|>|<=|>=|=="[<version>|<revision>]')
              raise ProgError

        # platform-specific database entry
        if remove_db_entry.systype:
          gnu_systype = get_gnu_systype (self.systype_path)
          if not systype_match (remove_db_entry.systype, gnu_systype):
            callback ('end', 'skipping', 2)
            callback ('note', '"' + gnu_systype +
                              '" does not match system type RE "' +
                      remove_db_entry.systype + '"', 2)
            continue

        # if dependency version met but not module
        version_met_but_not_module = []

        # if dependency version met but not install-name
        version_met_but_not_install_name = []

        # search for program matching test criteria in install base
        for sbdb_entry in sbdb_entries:
          if sbdb_entry.prog_name == remove_db_entry.program:
            if version.met (remove_db_entry.test.split (),
                            Version (sbdb_entry.prog_ver),
                            Version (sbdb_entry.prog_rev)):
              if not sbdb_entry.modules.has_key (remove_db_entry.module):
                module_str = []
                for module in sbdb_entry.modules.keys ():
                  sbdb_module = sbdb_entry.modules[module]
                  module_str.append ('module="' + module + '"')
                version_met_but_not_module.append (sbdb_entry.prog_ver + '-' +
                                                   str (sbdb_entry.prog_rev) +
                                                   ' (' + string.join \
                                                   (module_str, '; ') + \
                                                   ')')
                continue

              sbdb_module = sbdb_entry.modules[remove_db_entry.module]

              remove_db_program = Prog (sbdb_entry.prog_name + '-' +
                                   sbdb_entry.prog_ver)
              remove_db_program.module = sbdb_module
              remove_db_program.prog_name = sbdb_entry.prog_name
              remove_db_program.prog_ver = sbdb_entry.prog_ver
              remove_db_program.install_path = self.install_path
              remove_db_program.dry_run = self.dry_run
              remove_db_program.sbdb = sbdb_entry

              module_install_name = SbDBInstallName \
                                    (sbdb_module.install_name.path,
                                     sbdb_module.install_name.version)

              if remove_db_entry.install_name.path:
                module_install_path = var_substitute \
                                      (remove_db_program,
                                       self.get_install_path \
                                       (module_install_name))
                if remove_db_entry.install_name.path != module_install_path:
                  version_met_but_not_install_name.append \
                  (sbdb_entry.prog_ver + '-' + str (sbdb_entry.prog_rev) +
                  ' (install-name="' + module_install_path + '")')
                  continue

              break
        else:
          if version_met_but_not_install_name:
            error_string = 'while the version was met, the \
install-name was not. the versions I know about are: ' + \
                           string.join (version_met_but_not_install_name,
                                      ', ') + '.'
          elif version_met_but_not_module:
            error_string = 'while the version was met, the module was \
not. the versions I know about are: ' + \
                           string.join (version_met_but_not_module, ', ') + '.'
          else:
            versions = []
            for sbdb_entry in sbdb_entries:
              if sbdb_entry.prog_name == remove_db_entry.program:
                versions.append (sbdb_entry.prog_ver + '-' +
                                 str (sbdb_entry.prog_rev))

            if versions:
              error_string = 'the versions I know about are: ' + \
                             string.join (versions, ', ') + '.'
            else:
              error_string = ''

          test_string = []
          test_string.append ('module="' + remove_db_entry.module + '"')
          if remove_db_entry.install_name.path:
            test_string.append ('install-name="' + \
                                remove_db_entry.install_name.path + '"')

          callback ('warning', 'while searching the installation \
database to satisfy requirements for the <remove-db-entry> entries, \
an entry could not be found. unable to find version in installation \
directory, ' + self.install_base.path + ', matching test criteria: ' +
                            string.join (test_string, ', ') + ', ' + \
                            english_test_string (remove_db_entry.test) + \
                    '. ' + error_string)
          continue

        remove_db_programs.append (remove_db_program)

    if self.module.proc['uninstall']:
      try:
        dependencies = get_dependencies (self, callback,
                                         run_state = 'install',
                                         script = 'uninstall')

        script, script_name = create_script (self, callback, 'uninstall',
                                             dependencies)

        execute_script (self, callback, 'uninstall', script_name,
                        dependencies)
      except ProgError:
        raise ProgUninstallError
    else:
      install_prefix = self.get_install_prefix ()
      if os.path.exists (install_prefix):
        if os.path.isdir (install_prefix):
          callback ('begin', 'removing ' + install_prefix)
          if not self.dry_run:
            os.chdir (var_substitute (self, self.install_base.path))
            misclib.rmdir (install_prefix)
          callback ('end', 'done')
        else:
          raise ProgUninstallError ('installation prefix ' +
                                    install_prefix +
                                    ' exists but is not a directory')
      else:
        raise ProgUninstallError ('installation prefix ' +
                                  install_prefix + ' does not exist')

    sbdb.remove_sbdb_install_entry (sbdb_entries, callback, self)

    if remove_db_programs:
      callback ('note', 'the following modules are being \
removed as a <remove-db-entry> entry exists for them', -2)

      for remove_db_program in remove_db_programs:
        callback ('begin', 'removing database entry for ' +
                          remove_db_program.prog_name + '-' +
                          remove_db_program.prog_ver + ' (module="' +
                  remove_db_program.module.name + '")')
        sbdb.remove_sbdb_install_entry (sbdb_entries, None,
                                        remove_db_program)
        callback ('end', 'done')

    # if module has no <uninstall> script, the "default" module
    # <uninstall> script is run or the installation prefix is
    # removed. if multiple modules registered for program, and
    # uninstalling the program would remove the entire program,
    # remove all modules with the same installation prefix.
    try:
      sbdb_entry = sbdb_entries[self.prog_name + '-' + self.prog_ver]
    except IndexError:
      return

    if self.module.name == 'default':
      note_printed = 0

      for module_name in sbdb_entry.modules.keys ():
        if not sbdb_entry.modules[module_name].proc['uninstall']:
          program = copy.deepcopy (self)
          program.module = program.sbdb.modules[module_name]
          program.prog_ver = program.sbdb.prog_ver
          if program.module.install_base:
            program.install_base = program.module.install_base
          else:
            program.install_base = self.install_base
          program.install_name = program.module.install_name

          if self.get_install_prefix () == program.get_install_prefix ():
            if not note_printed:
              callback ('note', 'the following modules are \
being removed because they share the same installation prefix as the \
"default" module', -2)
              note_printed = 1
            sbdb.remove_sbdb_install_entry (sbdb_entries, callback,
                                            program)

            # don't try to uninstall module
            if module_name in self.modules:
              self.modules.remove (module_name)

  def unpack (self, module, callback = None):
    # remove existing build directory
    if os.path.exists (self.build_prefix) and \
    self.builddir != self.build_prefix:
      try:
        self.purge (callback)
      except ProgPurgeError:
        raise ProgUnpackError

    # while we don't use the dependencies unpacking the source
    # files, we check to make sure they are present before
    # going through the effort of unpacking
    try:
      get_dependencies (self, callback, run_state = 'build')
    except ProgError:
      raise ProgUnpackError

    # extract source files in archive to temporary directory.
    # makes it easy for tar/zip to work on.
    extract_source (self, callback)

    # remains false if all sources are noop="1".
    unpacked_to_build_prefix = False

    for source in self.module.sources:
      type = source[0]			# 'source' or 'patch'
      path = var_substitute (self, source[1].path)
      arg = source[1].arg
      checksum = source[1].checksum
      checksum_type = source[1].checksum_type
      encoding = source[1].encoding
      size = source[1].size

      # path either absolute or relative to depot directory
      # where xml file found
      if os.path.isabs (path):
        src = path
      else:
        src = self.srcdir + '/' + path

      if source[1].destdir:
        destdir = var_substitute (self, source[1].destdir)

        if type == 'source':
          if os.path.isabs (destdir):
            build_prefix = destdir
          else:
            build_prefix = os.path.join (self.builddir, destdir)
        else:
          if source[1].build_name:
            build_prefix = os.path.join (self.get_build_prefix (source[1].build_name), destdir)
          else:
            build_prefix = os.path.join (self.build_prefix, destdir)

        # create build directory if it doesn't exist. needed if
        # the source files unpack into the parent directory and
        # we'd rather unpack to a directory we create.
        if not os.path.exists (build_prefix) and not self.dry_run:
          os.makedirs (build_prefix)
      else:
        if type == 'source':
          build_prefix = self.builddir
        else:
          if source[1].build_name:
            build_prefix = self.get_build_prefix (source[1].build_name)
          else:
            build_prefix = self.build_prefix

      # skip placeholder sources (usually unpacked at install time
      # manually)
      if source[1].noop == 1:
        continue

      unpacked_to_build_prefix = True

      # quilt-style patches?
      _quilt_patch = False
      if src.endswith ('/'):
        if self.quilt and os.path.isdir (src):
          callback ('end', 'unpacking quilt patches from ' +
                            os.path.join (path, self.quilt_patches))
          _quilt_patch = True
        elif not self.quilt:
          callback ('error', 'patch file "' + src +
                            '" ends with a "/" but the "quilt" \
variable in the config file is not set.')
          raise ProgUnpackError
        elif not os.path.isdir (src):
          callback ('error', 'patch file "' + src +
                            '" ends with a "/" but it is not a \
directory.')
          raise ProgUnpackError
      else:
        callback ('end', 'unpacking ' + path)

      # platform-specific source
      if source[1].systype:
        gnu_systype = get_gnu_systype (self.systype_path)
        if not systype_match (source[1].systype, gnu_systype):
          callback ('end', 'skipping as "' + gnu_systype +
          '" does not match system type RE "' + source[1].systype + '"', 4)
          continue

      # unpack source
      if self.verbose:
        callback ('end', '$ cd ' + build_prefix, 2)

      if not self.dry_run:
        pwd = os.getcwd ()
        try:
          os.chdir (build_prefix)
        except (IOError, OSError), msg:
          callback ('error', 'unable to change to build \
directory "' + build_prefix + '". error message from chdir command: ' +
                    str (msg))
          raise ProgUnpackError

      if _quilt_patch:
        series = os.path.join (src, self.quilt_patches, "series")
        try:
          fh = open (series, 'r')
        except IOError, msg:
          callback ('error', 'error opening quilt series \
file "' + series + '". error was: ' + str (msg))
          raise ProgUnpackError

        while 1:
          patch_file = fh.readline ().strip ().split (' ')[0]
          if not patch_file:
            break

          try:
            self.unpack_file (source, os.path.join (src, self.quilt_patches,
                                                    patch_file),
                              build_prefix, module, callback)
          except ProgUnpackFileError, msg:
            callback ('error', str (msg))
            fh.close ()
            raise ProgUnpackError

        fh.close ()
      else:
        try:
          self.unpack_file (source, src, build_prefix, module, callback)
        except ProgUnpackFileError, msg:
          callback ('error', str (msg))
          raise ProgUnpackError

      if not self.dry_run:
        os.chdir (pwd)

    # if all source and patch entries were place-holders, then create the
    # build-prefix directory manually so that <configure> can unpack unsupported
    # payload formats:
    if not os.path.exists (self.build_prefix) and \
    not self.dry_run and not unpacked_to_build_prefix: 
      callback ('begin', 'no source to unpack, creating ' + self.build_prefix) 
      os.makedirs (self.build_prefix) 
      callback ('end', 'done') 

    # update database entry
    sbdb.add_sbdb_entry (self.sbdb_build_entries, 'build', callback, self)

  # unpack source/patch file
  def unpack_file (self, source, src, build_prefix, module, callback):
    type = source[0]			# 'source' or 'patch'
    arg = source[1].arg
    checksum = source[1].checksum
    checksum_type = source[1].checksum_type
    encoding = source[1].encoding
    size = source[1].size

    try:
      src_archive = prog.archive.init (src, type)
    except prog.ProgArchiveError, msg:
      raise ProgUnpackFileError, (str (msg))

    src_archive.build_prefix = build_prefix
    src_archive.srcdir = self.srcdir
    src_archive.arg = arg
    src_archive.checksum = checksum
    src_archive.checksum_type = checksum_type
    src_archive.depot = self.depot
    src_archive.module = module
    src_archive.sbdb_filename = self.depot.local_path + '/' + \
                                self.sbdb.filename
    src_archive.size = size
    src_archive.environ = make_environ (self)
    src_archive.bash_path = self.bash_path

    try:
      if not self.dry_run:
        src_archive.write_build_prefix ()
      src_archive.unpack (self.dry_run, self.verbose, callback)
    except ProgPayloadNotFound:
      raise ProgUnpackFileError, ('neither source file ' + src +
            ' nor payload found')
    except ProgProgramNotFound, msg:
      raise ProgUnpackFileError ('unable to locate path to "' +
            str (msg) + '"')
    except ProgArchiveError, msg:
      raise ProgUnpackFileError (str (msg))
    except ProgPayloadInvalidSize, msg:
      raise ProgUnpackFileError ('Payload size does not match. The \
expected file size is ' + str (size) + ' bytes but the \
file size of what was extracted was ' + str (msg.size) + '.')
    except ProgPayloadInvalidChecksum, msg:
      raise ProgUnpackFileError ('Payload checksum does not match. \
The expected checksum is ' + checksum + ' but the checksum \
calculated was ' + msg.checksum + '.')

# create script
def create_script (program, callback, action, dependencies,
                   default_script = None):
  callback ('begin', 'creating ' + action + ' script')
  script, script_name = misclib.mkstempfile ()
  mysig.register (os.unlink, script_name)

  # build variables
  script.write ('\
#!/bin/sh\n\
\n\
##\n\
## Bash-specific variables\n\
##\n\
set -o nounset			# error on unset variables\n\
set -o privileged		# restrict what we inherit\n\
\n\
##\n\
## Default umask\n\
##\n\
umask ' + oct (program.umask) + '\n\
\n\
##\n\
## distribution name\n\
##\n\
SB_DIST=' + program.dist + '\n\
\n\
##\n\
## program information\n\
##\n\
SB_PROG_NAME=' + program.prog_name + '\n\
SB_PROG_VER=' + program.prog_ver + '\n\
NAME=' + program.prog_name + '\n\
VER=' + program.prog_ver + '\n\
\n\
##\n\
## module information\n\
##\n\
SB_MODULE_NAME=' + program.module.name + '\n\
\n\
##\n\
## tools we provide\n\
##\n\
SB_PATH_AWK="%AWK%"\n\
SB_PATH_CHGRP="%CHGRP%"\n\
SB_PATH_CHOWN="%CHOWN%"\n\
SB_PATH_CHMOD="%CHMOD%"\n\
SB_PATH_CPIO="%CPIO%"\n\
SB_PATH_EGREP="%EGREP%"\n\
SB_PATH_FIND="%FIND%"\n\
SB_PATH_INSTALL="%INSTALL%"\n\
SB_PATH_LN="%LN%"\n\
SB_PATH_TAR="%TAR%"\n\
SB_PATH_TOUCH="%TOUCH%"\n\
SB_PATH_GCHGRP="%CHGRP%"\n\
SB_PATH_GCHOWN="%CHOWN%"\n\
SB_PATH_GCHMOD="%CHMOD%"\n\
SB_PATH_GFIND="%FIND%"\n\
SB_PATH_GGREP="%EGREP%"\n\
SB_PATH_GINSTALL="%INSTALL%"\n\
SB_PATH_GLN="%LN%"\n\
SB_PATH_GTAR="%TAR%"\n\
SB_PATH_GTOUCH="%TOUCH%"\n\
SB_PATH_SB="%bindir%/sb"\n\
SB_PATH_SED="%SED%"\n\
\n\
##\n\
## system type\n\
##\n\
SB_SYSTYPE=' + os.popen (program.systype_path, 'r').readline () + '\
SYSTYPE=' + os.popen (program.systype_path, 'r').readline () + '\
\n\
##\n\
## prefixes\n\
##\n\
SB_BUILD_BASE=' + program.builddir + '\n\
SB_BUILD_NAME=' + var_substitute (program,
                                  program.module.build_name) + '\n\
SB_BUILD_PREFIX=' + program.build_prefix + '\n\
SB_INSTALL_BASE=' + var_substitute (program,
                                    program.install_prefix_base.path) + '\n\
SB_INSTALL_NAME=' + var_substitute (program,
                                    program.module.install_name.path) + '\n\
SB_INSTALL_PATH=' + var_substitute (program,
                                    program.get_install_path ()) + '\n\
SB_INSTALL_PREFIX=' + program.get_install_prefix () + '\n\
SB_INSTALL_PREFIX_INC=' + program.get_install_prefix () + '/include\n\
SB_INSTALL_PREFIX_LIB=' + os.path.join (program.get_install_prefix (),
                                        program.lib_path_component) + '\n\
BUILD_BASE=' + program.builddir + '\n\
BUILD_NAME=' + var_substitute (program,
                               program.module.build_name) + '\n\
BUILD_PREFIX=' + program.build_prefix + '\n\
INSTALL_BASE=' + var_substitute (program,
                                 program.install_prefix_base.path) + '\n\
INSTALL_NAME=' + var_substitute (program,
                                 program.module.install_name.path) + '\n\
INSTALL_PATH=' + var_substitute (program,
                                 program.get_install_path ()) + '\n\
INSTALL_PREFIX=' + program.get_install_prefix () + '\n\
\n\
##\n\
## user functions\n\
##\n\
\n\
# output message to STDERR with word wrapping\n\
function sb_msg_err () {\n\
local _msg="$1"\n\
\n\
set -- ${IFS}\n\
_ifs="$@" IFS=''\n\
echo "${_msg}" | fold -s -w 72 | while read line; do\n\
  echo "sb: $line" >&2\n\
done\n\
IFS="${_ifs}"\n\
}\n\
\n\
\n')

  dependencies_comment_printed = 0
  if dependencies:
    for depend in dependencies:
      # ignore dependencies that don't need a variable set
      if not depend.var:
        continue

      if not dependencies_comment_printed:
        dependencies_comment_printed = 1
        script.write ('# dependencies\n')

      if depend.set_var_only == 1:
        script.write ('SB_VAR_' + depend.var + '=' +
                      depend.install_prefix + '\n')
        script.write (depend.var + '=' + depend.install_prefix + '\n')
      elif not depend.path:
        if os.path.isdir (depend.install_prefix):
          script.write ('SB_VAR_' + depend.var + '=' +
                        depend.install_prefix + '\n')
          script.write (depend.var + '=' + depend.install_prefix + '\n')
        if os.path.isdir (depend.install_prefix + '/include'):
          script.write ('SB_VAR_' + depend.var + '_INC=' +
                        depend.install_prefix + '/include\n')
          script.write (depend.var + '_INC=' +
                        depend.install_prefix + '/include\n')
        if depend.lib_search_path_components:
          _lib_search_path_components = \
            depend.lib_search_path_components.split ()
        else:
          _lib_search_path_components = program.lib_search_path_components
        for _search_lib in _lib_search_path_components:
          if os.path.isdir (os.path.join (depend.install_prefix,
                                          _search_lib)):
            script.write ('SB_VAR_' + depend.var + '_LIB=' +
                          os.path.join (depend.install_prefix,
                                        _search_lib) + '\n')
            script.write (depend.var + '_LIB=' +
                          os.path.join (depend.install_prefix,
                                        _search_lib) + '\n')
            break

    if dependencies_comment_printed:
      script.write ('\n')

  # set source variables
  print_src_var_comment = 0
  for source in program.module.sources:
    path = var_substitute (program, source[1].path)
    var = source[1].var

    if var:
      if not print_src_var_comment:
        script.write ('# marked source files\n')
        print_src_var_comment = 1

      # path either absolute or relative to depot directory
      # where xml file found
      if os.path.isabs (path):
        src = path
      else:
        src = program.srcdir + '/' + path

      script.write ('\
SB_SRC_' + var + '="' + src + '"\n\
SRC_' + var + '="' + src + '"\n')

  # configure, build, test, and install from within build prefix
  if action in ['configure', 'build', 'test', 'install']:
    script.write ('\n\
# configure, build, test, and install from within build prefix\n\
if [ -d "${SB_BUILD_PREFIX}" ]; then\n\
  cd "${SB_BUILD_PREFIX}" || exit 1\n\
fi\n')

  if program.module.proc['script-header']:
    if not program.module.proc['script-header'].replace:
      if program.sbdb.d_proc['script-header']:
        script.write ('\n\
' + program.sbdb.d_proc['script-header'].data + '\n')
    script.write ('\n\
' + program.module.proc['script-header'].data + '\n')
  if program.module.proc[action] is not None:
    script.write ('\n\
' + program.module.proc[action].data + '\n')
  elif default_script:
    script.write ('\n' + default_script + '\n')

  script.close ()

  callback ('end', script_name)

  return script, script_name

# execute script
def execute_script (program, callback, action, script_name, dependencies):
  # execute configure script with custom environment
  callback ('end', 'executing ' + action + ' script ...')
  environ = make_environ (program)

  # add dependency paths to $PATH
  if dependencies:
    for depend in dependencies:
      if depend.add_path == 'prepend':
        if depend.install_name.append:
          environ['PATH'] = depend.install_prefix + ':' + environ['PATH']
        else:
          environ['PATH'] = depend.install_prefix + '/bin:' + \
                            environ['PATH']
      elif depend.add_path == 'append':
        if depend.install_name.append:
          environ['PATH'] += ':' + depend.install_prefix
        else:
          environ['PATH'] += ':' + depend.install_prefix + '/bin'

  # execute all scripts with bash
  if not program.bash_path:
    program.bash_path = misclib.find_path_prog ('bash')
    if not program.bash_path:
      callback ('error', 'unable to locate path to bash')
      raise ProgError

  environ['SHELL'] = program.bash_path
  environ['CONFIG_SHELL'] = program.bash_path

  if program.dry_run:
    if program.verbose >= VERBOSE_DEBUG:
      callback ('end', '$ ' + program.bash_path + \
                ' --noprofile --norc -x ' + script_name, 2)
    else:
      callback ('end', '$ ' + program.bash_path + \
                ' --noprofile --norc ' + script_name, 2)
  else:
    if program.verbose >= VERBOSE_DEBUG:
      rc = os.spawnve (os.P_WAIT, program.bash_path, [program.bash_path,
      '--noprofile', '--norc', '-x', script_name], environ)
    else:
      rc = os.spawnve (os.P_WAIT, program.bash_path, [program.bash_path,
      '--noprofile', '--norc', script_name], environ)

  mysig.unregister (os.unlink)
  os.unlink (script_name)

  if not program.dry_run and rc:
    callback ('error', 'error executing script')
    raise ProgError

# get list of dependencies
def get_dependency_list (dependencies, module_dependencies,
                         default_dependencies):
  dependencies_replace = dependencies and dependencies.replace
  module_replace = module_dependencies and module_dependencies.replace

  dependency_list = []

  if not dependencies_replace:
    if not module_replace:

      # Start with the default dependencies, unless either module or
      # specific dependencies are set to replace them:
      if default_dependencies:
        for depend in default_dependencies:
          dependency_list.append (depend)

    # Continue with the module dependencies, unless the specific
    # dependencies are set to replace them:
    if module_dependencies:
      for depend in module_dependencies:
        dependency_list.append (depend)

  # End with the specific dependencies, if any:
  if dependencies:
    for depend in dependencies:
      dependency_list.append (depend)

  return dependency_list

# find dependencies in dependency list
def get_dependencies (program, callback, run_state, script = None):
  dependencies = []

  if script and program.module.proc[script]:
    script_dependencies = program.module.proc[script].dependencies
  else:
    script_dependencies = None               

  dependencies = get_dependency_list (script_dependencies,
                                      program.module.dependencies,
                                      program.sbdb.d_dependencies)

  if not dependencies:
    return None

  callback ('end', 'locating dependencies ...')
  return verify_dependencies (program, dependencies, run_state,
                              callback)

# verify dependencies
def verify_dependencies (program, dependencies, run_state, callback,
                         indent = 0, print_path_if_found = 1):
  dependency_list = []

  for depend in dependencies:
    # validate test argument
    if depend.test:
      for test in depend.test.split ():
        test_var = re.sub ('^(\w)([>=<]+)(.*)', '\\1', test)
        test_compare = re.sub ('^(\w)([>=<]+)(.*)', '\\2', test)

        if (test_var != 'v' and test_var != 'r') or \
        (test_compare != '<' and test_compare != '>' and \
        test_compare != '<=' and test_compare != '>=' and \
        test_compare != '=='):
          callback ('error', 'dependency test not correctly \
formatted. while "' + test + '" was given, the correct format is: \
[v|r]"<|>|<=|>=|=="[<version>|<revision>]', indent)
          raise ProgError

    # user dependency
    if depend.user:
      callback ('begin', 'searching for user "' +
                depend.user + '"', 2 + indent)

      try:
        pwd.getpwnam (depend.user)
      except KeyError:
        callback ('end', 'not found', 2 + indent)
        callback ('error', 'unable to find required \
user "' + depend.user + '"', 2 + indent)
        raise ProgError
      callback ('end', 'found', 2 + indent)
      continue

    # group dependency
    if depend.group:
      callback ('begin', 'searching for group "' +
                depend.group + '"', 2 + indent)

      try:
        grp.getgrnam (depend.group)
      except KeyError:
        callback ('end', 'not found', 2 + indent)
        callback ('error', 'unable to find required \
group "' + depend.group + '"', 2 + indent)
        raise ProgError
      callback ('end', 'found', 2 + indent)
      continue

    if depend.program:
      if depend.module != 'default':
        callback ('begin', 'searching for "' +
                          depend.program + '" (module="' +
                  depend.module + '")', 2 + indent)
      else:
        callback ('begin', 'searching for "' +
                  depend.program + '"', 2 + indent)

      # platform-specific dependency
      if depend.systype:
        gnu_systype = get_gnu_systype (program.systype_path)
        if not systype_match (depend.systype, gnu_systype):
          callback ('end', 'skipping', 2 + indent)
          callback ('note', '"' + gnu_systype +
                            '" does not match system type RE "' +
                    depend.systype + '"', 2 + indent)
          continue

      # ignore build dependencies in install state
      if run_state == 'install' and depend.state != 'install':
        callback ('end', 'ignored (skipping)', 2 + indent)
        callback ('note', 'the state dependency is "' +
                          depend.state + '" but installation is \
occurring so this dependency is ignored', 2 + indent)
        continue

    # override default installation base?
    if depend.install_base:
      install_base = SbDBInstallBase (var_substitute (program,
                                                      depend.install_base.path))
    else:
      install_base = program.install_base

    if depend.program:
      # which database depends on state attribute of dependency
      if depend.state == 'install':
        if not program.sbdb_install_entries.has_key (install_base.path):
          program.sbdb_install_entries[install_base.path] = \
            sbdb.read_sbdb_entries (install_base.path,
                                    program.depot.dtd_dirs)
        sbdb_entries = program.sbdb_install_entries[install_base.path]
      else:
        sbdb_entries = program.sbdb_build_entries

      # if dependency version met but state not met
      version_met_but_not_state = []

      # if dependency version met but not module
      version_met_but_not_module = []

      # if dependency version met but not install-name
      version_met_but_not_install_name = []

      # search for program matching test criteria in install base
      for sbdb_entry in sbdb_entries:
        if sbdb_entry.prog_name == depend.program:
          if version.met (depend.test.split (),
                          Version (sbdb_entry.prog_ver),
                          Version (sbdb_entry.prog_rev)):
            if not sbdb_entry.modules.has_key (depend.module):
              module_state_str = []
              for module in sbdb_entry.modules.keys ():
                sbdb_module = sbdb_entry.modules[module]
                module_state_str.append ('module="' + module + \
                                         '", state="' + sbdb_module.state + \
                                         '"')
              version_met_but_not_module.append (sbdb_entry.prog_ver + '-' +
                                                 str (sbdb_entry.prog_rev) +
                                                 ' (' + string.join \
                                                 (module_state_str, '; ') + \
                                                 ')')
              continue
            
            sbdb_module = sbdb_entry.modules[depend.module]

            depend_program = Prog (sbdb_entry.prog_name + '-' +
                                   sbdb_entry.prog_ver)
            depend_program.module = sbdb_module
            depend_program.prog_name = sbdb_entry.prog_name
            depend_program.prog_ver = sbdb_entry.prog_ver
            depend_program.install_path = program.install_path

            module_install_name = SbDBInstallName \
                                  (sbdb_module.install_name.path,
                                   sbdb_module.install_name.version)
            depend_install_name = SbDBInstallName \
                                  (depend.install_name.path,
                                   depend.install_name.version)

            if depend.install_name.path:
              module_install_path = var_substitute (depend_program,
                                                    program.get_install_path (module_install_name))
              depend_install_path = depend_program.get_install_path (depend_install_name)
              if depend_install_path != module_install_path:
                version_met_but_not_install_name.append \
                (sbdb_entry.prog_ver + '-' + str (sbdb_entry.prog_rev) +
                ' (install-name="' + module_install_path + '")')
                continue

            if _state_index[sbdb_module.state] >= _state_index[depend.state]:
              dep_install_name = copy.deepcopy (sbdb_module.install_name)
              if depend.install_name.append:
                dep_install_name.append = depend.install_name.append

              break
            else:
              version_met_but_not_state.append (sbdb_entry.prog_ver + '-' +
                                                str (sbdb_entry.prog_rev) +
                                                ' (state="' +
                                                sbdb_module.state + '")')
      else:
        if depend.require == 1:
          callback ('end', 'not found', 2 + indent)
        else:
          callback ('end', 'not found (skipping)', 2 + indent)

        if version_met_but_not_state:
          error_string = 'while the version was met, the state was \
not. the versions I know about are: ' + \
                         string.join (version_met_but_not_state, ', ') + '.'
        elif version_met_but_not_install_name:
          error_string = 'while the version was met, the \
install-name was not. the versions I know about are: ' + \
                         string.join (version_met_but_not_install_name,
                                      ', ') + '.'
        elif version_met_but_not_module:
          error_string = 'while the version was met, the module was \
not. the versions I know about are: ' + \
                         string.join (version_met_but_not_module, ', ') + '.'
        else:
          versions = []
          for sbdb_entry in sbdb_entries:
            if sbdb_entry.prog_name == depend.program:
              versions.append (sbdb_entry.prog_ver + '-' +
                               str (sbdb_entry.prog_rev))

          if versions:
            error_string = 'the versions I know about are: ' + \
                           string.join (versions, ', ') + '.'
          else:
            error_string = ''

        test_string = []
        test_string.append ('module="' + depend.module + '"')
        test_string.append ('state="' + depend.state + '"')
        if depend.install_name.path:
          test_string.append ('install-name="' + \
                                       depend.install_name.path + '"')

        if depend.require == 1:
          error_type = 'error'
        else:
          error_type = 'warning'

        if depend.state == 'install':
          callback (error_type, 'unable to find version in \
installation directory, ' + install_base.path + ', matching \
test criteria: ' + string.join (test_string, ', ') + ', ' + \
                            english_test_string (depend.test) + \
                    '. ' + error_string, 2 + indent)
        else:
          callback (error_type, 'unable to find version in build \
directory, ' + program.builddir + ', matching \
test criteria: ' + string.join (test_string, ', ') + ', ' + \
                            english_test_string (depend.test) + \
                    '. ' + error_string, 2 + indent)
        if depend.require == 1:
          raise ProgError
        else:
          continue
    else:
      dep_install_name = copy.deepcopy (program.module.install_name)
      if depend.install_name.append:
        dep_install_name.append = depend.install_name.append
      if depend.install_name.path:
        dep_install_name.path = depend.install_name.path
      if depend.install_name.version:
        dep_install_name.version = depend.install_name.version

    if depend.path:
      if dep_install_name.append:
        depend_path = os.path.join (depend.path, dep_install_name.append)
      else:
        depend_path = depend.path
    elif depend.state == 'install':
      if depend.program:
        prog = depend_program
      else:
        prog = program

      if depend.var_substitute:
        depend_path = prog.get_install_prefix \
                      (install_base, dep_install_name)
      else:
        depend_path = os.path.normpath \
                      (os.path.join \
                      (install_base, prog.get_install_path \
                                  (dep_install_name)))
    else:
      depend_program = Prog (sbdb_entry.prog_name + '-' +
                             sbdb_entry.prog_ver)
      depend_program.builddir = program.builddir
      depend_program.module = sbdb_module
      depend_program.prog_name = sbdb_entry.prog_name
      depend_program.prog_ver = sbdb_entry.prog_ver

      depend_program.build_prefix = depend_program.get_build_prefix ()
      depend_path = depend_program.build_prefix

    if depend.set_var_only == 1:
      callback ('end', 'setting variable:', 2 + indent)
      callback ('end', 'SB_VAR_' + depend.var + '=' + depend_path,
                4 + indent)
    elif depend.path:
      callback ('begin', 'searching for "' + depend_path + '"', 2 + indent)
      callback ('end', 'found', 2 + indent)
    else:
      if not depend.program:
        callback ('begin', 'searching for "' + depend_path + '"', 2 + indent)

        # platform-specific dependency
        if depend.systype:
          gnu_systype = get_gnu_systype (program.systype_path)
          if not systype_match (depend.systype, gnu_systype):
            callback ('end', 'skipping', 2 + indent)
            callback ('note', '"' + gnu_systype +
                              '" does not match system type RE "' +
                      depend.systype + '"', 2 + indent)
            continue

      if not os.path.exists (depend_path):
        if depend.require == 1:
          callback ('end', 'not found', 2 + indent)
          callback ('error', 'unable to find required \
dependency "' + depend_path + '"', 2 + indent)
          raise ProgError
        else:
          callback ('end', 'not found (skipping)', 2 + indent)
          callback ('error', 'the dependency criteria for \
this program was met but the installation prefix, ' + depend_path + \
                            ', was not found. this should not occur. \
please remove the database entry for this program.', 2 + indent)
          raise ProgError
      else:
        if not depend.program or not print_path_if_found:
          callback ('end', 'found', 2 + indent)
        else:
          callback ('end', depend_path, 2 + indent)

    depend.install_prefix = depend_path
    dependency_list.append (depend)

  return dependency_list

# create custom environment for executing scripts
def make_environ (program):
  environ = {}
  for env in ['DISPLAY', 'EDITOR', 'HOME', 'IFS', 'LANG',
  'PATH', 'TERM', 'TTY', 'TZ', 'XAUTHORITY']:
    if os.environ.has_key (env):
      environ[env] = os.environ[env]

  # add environment vars specific in config file
  if program.environ:
    for env in program.environ.keys ():
      environ[env] = program.environ[env]

  return environ

# ensure sources unpacked
def prep (program, callback, run_state, script):
  if not program.dry_run and program.module.sources and \
  not os.path.exists (program.build_prefix):
    callback ('error', 'build prefix "' + program.build_prefix +
              '" does not exist. did you --unpack to unpack the sources?')
    raise ProgError

  return get_dependencies (program, callback, run_state, script)

# perform variable substitution on string
def var_substitute (program, string):
  if string is None:
    return ''

  if program.builddir:
    string = re.sub ('\${SB_BUILD_BASE}', program.builddir, string)
    string = re.sub ('\${BUILD_BASE}', program.builddir, string)

  string = re.sub ('\${SB_MODULE_NAME}', program.module.name, string)
  string = re.sub ('\${MODULE_NAME}', program.module.name, string)
  string = re.sub ('\${SB_PROG_NAME}', program.prog_name, string)
  string = re.sub ('\${NAME}', program.prog_name, string)
  string = re.sub ('\${SB_PROG_VER}', program.prog_ver, string)
  string = re.sub ('\${VER}', program.prog_ver, string)

  return string

# extract source files in archive to temporary directory.
# makes it easy for tar/zip to work on.
def extract_source (program, callback):
  if program.archive:
    filelist = []

    for source in program.module.sources:
      path = var_substitute (program, source[1].path)
      if os.path.isabs (path):
        continue

      filelist.append (path)

    program.srcdir = misclib.mkstempdir ()

    # mysig.register makes sure that the temporary directory is
    # cleaned up in case of abnormal exit, and atexit.register
    # makes sure of the same when the program exits normally.
    mysig.register (program.cleanup, callback)
    atexit.register (program.cleanup, callback)

    callback ('end', 'extracting source files ...')

    sbdb_archive = prog.archive.init (program.archive.path)
    sbdb_archive.module = program.module.name
    sbdb_archive.environ = make_environ (program)
    sbdb_archive.unpack (program.dry_run, program.verbose, callback,
                         program.srcdir, filelist, list_files = False)
